1 /** 2 Creates test cases from compile-time information. 3 */ 4 module unit_threaded.factory; 5 6 import unit_threaded.from; 7 import unit_threaded.testcase : CompositeTestCase; 8 9 private CompositeTestCase[string] serialComposites; 10 11 /** 12 * Creates tests cases from the given modules. 13 * If testsToRun is empty, it means run all tests. 14 */ 15 from!"unit_threaded.testcase".TestCase[] createTestCases( 16 in from!"unit_threaded.reflection".TestData[] testData, in string[] testsToRun = []) { 17 import unit_threaded.testcase : TestCase; 18 import std.algorithm : sort; 19 import std.array : array; 20 21 serialComposites = null; 22 bool[TestCase] tests; 23 foreach (const data; testData) { 24 if (!isWantedTest(data, testsToRun)) 25 continue; 26 auto test = createTestCase(data); 27 if (test !is null) 28 tests[test] = true; //can be null if abtract base class 29 } 30 31 return tests.keys.sort!((a, b) => a.getPath < b.getPath).array; 32 } 33 34 package from!"unit_threaded.testcase".TestCase createTestCase( 35 in from!"unit_threaded.reflection".TestData testData) { 36 import unit_threaded.testcase : TestCase; 37 import std.algorithm : splitter, reduce; 38 import std.array : array; 39 40 TestCase createImpl() { 41 import unit_threaded.testcase : BuiltinTestCase, FunctionTestCase, 42 ShouldFailTestCase, FlakyTestCase; 43 import std.conv : text; 44 45 TestCase testCase; 46 47 if (testData.isTestClass) 48 testCase = cast(TestCase) Object.factory(testData.name); 49 else 50 testCase = testData.builtin ? new BuiltinTestCase(testData) 51 : new FunctionTestCase(testData); 52 53 version (unitThreadedLight) { 54 } else 55 assert(testCase !is null, text("Error creating test case with ", 56 testData.isTestClass ? "test class data: " : "data: ", testData)); 57 58 if (testData.shouldFail) { 59 testCase = new ShouldFailTestCase(testCase, testData.exceptionTypeInfo); 60 } else if (testData.flakyRetries > 0) 61 testCase = new FlakyTestCase(testCase, testData.flakyRetries); 62 63 return testCase; 64 } 65 66 auto testCase = createImpl(); 67 68 if (testData.singleThreaded) { 69 // @Serial tests in the same module run sequentially. 70 // A CompositeTestCase is created for each module with at least 71 // one @Serial test and subsequent @Serial tests 72 // appended to it 73 //const moduleName = testData.name.dup.splitter(".") 74 const moduleName = testData.name.splitter(".").array[0 .. $ - 1].reduce!((a, 75 b) => a ~ "." ~ b); 76 77 // create one if not already there 78 if (moduleName !in serialComposites) { 79 serialComposites[moduleName] = new CompositeTestCase; 80 } 81 82 // add the current test to the composite 83 serialComposites[moduleName] ~= testCase; 84 return serialComposites[moduleName]; 85 } 86 87 assert(testCase !is null || testData.testFunction is null, 88 "Could not create TestCase object for test " ~ testData.name); 89 90 return testCase; 91 } 92 93 private bool isWantedTest(in from!"unit_threaded.reflection".TestData testData, 94 in string[] testsToRun) { 95 96 import std.algorithm : filter, all, startsWith, canFind; 97 import std.array : array; 98 99 bool isTag(in string t) { 100 return t.startsWith("@") || t.startsWith("~@"); 101 } 102 103 auto normalToRun = testsToRun.filter!(a => !isTag(a)).array; 104 auto tagsToRun = testsToRun.filter!isTag; 105 106 bool matchesTags(in string tag) { //runs all tests with the specified tags 107 assert(isTag(tag)); 108 return tag[0] == '@' && testData.tags.canFind(tag[1 .. $]) 109 || (!testData.hidden && tag.startsWith("~@") && !testData.tags.canFind(tag[2 .. $])); 110 } 111 112 return isWantedNonTagTest(testData, normalToRun) && (tagsToRun.empty 113 || tagsToRun.all!(t => matchesTags(t))); 114 } 115 116 private bool isWantedNonTagTest( 117 in from!"unit_threaded.reflection".TestData testData, in string[] testsToRun) { 118 119 import std.algorithm : any, startsWith, canFind; 120 121 if (!testsToRun.length) 122 return !testData.hidden; //all tests except the hidden ones 123 124 bool matchesExactly(in string t) { 125 return t == testData.name; 126 } 127 128 bool matchesPackage(in string t) { //runs all tests in package if it matches 129 with (testData) 130 return !hidden && name.length > t.length && name.startsWith(t) 131 && name[t.length .. $].canFind("."); 132 } 133 134 return testsToRun.any!(a => matchesExactly(a) || matchesPackage(a)); 135 } 136 137 unittest { 138 import unit_threaded.reflection : TestData; 139 140 //existing, wanted 141 assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests"])); 142 assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests."])); 143 assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests.server.testSubscribe"])); 144 assert(!isWantedTest(TestData("tests.server.testSubscribe"), 145 ["tests.server.testSubscribeWithMessage"])); 146 assert(!isWantedTest(TestData("tests.stream.testMqttInTwoPackets"), ["tests.server"])); 147 assert(isWantedTest(TestData("tests.server.testSubscribe"), ["tests.server"])); 148 assert(isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests"])); 149 assert(isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests.testEqual"])); 150 assert(isWantedTest(TestData("pass_tests.testEqual"), [])); 151 assert(!isWantedTest(TestData("pass_tests.testEqual"), ["pass_tests.foo"])); 152 assert(!isWantedTest(TestData("example.tests.pass.normal.unittest"), 153 ["example.tests.pass.io.TestFoo"])); 154 assert(isWantedTest(TestData("example.tests.pass.normal.unittest"), [])); 155 assert(!isWantedTest(TestData("tests.pass.attributes.testHidden", null, true /*hidden*/ ), 156 ["tests.pass"])); 157 assert(!isWantedTest(TestData("", null, false /*hidden*/ , false /*shouldFail*/ , 158 false /*singleThreaded*/ , false /*builtin*/ , "" /*suffix*/ ), ["@foo"])); 159 assert(isWantedTest(TestData("", null, false /*hidden*/ , false /*shouldFail*/ , 160 false /*singleThreaded*/ , false /*builtin*/ , "" /*suffix*/ , ["foo"]), ["@foo"])); 161 162 assert(!isWantedTest(TestData("", null, false /*hidden*/ , false /*shouldFail*/ , 163 false /*singleThreaded*/ , false /*builtin*/ , "" /*suffix*/ , ["foo"]), ["~@foo"])); 164 165 assert(isWantedTest(TestData("", null, false /*hidden*/ , false /*shouldFail*/ , 166 false /*singleThreaded*/ , false /*builtin*/ , "" /*suffix*/ ), ["~@foo"])); 167 168 assert(isWantedTest(TestData("", null, false /*hidden*/ , false /*shouldFail*/ , 169 false /*singleThreaded*/ , false /*builtin*/ , "" /*suffix*/ , ["bar"]), ["~@foo"])); 170 171 // if hidden, don't run by default 172 assert(!isWantedTest(TestData("", null, true /*hidden*/ , false /*shouldFail*/ , 173 false /*singleThreaded*/ , false /*builtin*/ , "" /*suffix*/ , ["bar"]), ["~@foo"])); 174 175 }